#include <segments.h>
#include <ide.h>
#include <elf.h>


#define	PROT_CSEG		0x8
#define	PROT_DSEG		0x10
#define	CR0_PE			0x1

#define	BLOCK_LEN		0x1FE
#define	BOOT_SIG		0xAA55	

#define KERNEL_START	0x2
#define KERNEL_HDR		0x10000	

#define BYTE_SH			0x8
#define SECT_SH			0x9		
#define SECT_LONGS		0x80
#define PAGE_SECTS		0x8
#define SECT_ROUND		0xFFFFFE00

#define MEM_DET_CMD		0xE801
#define MEM_DET_PG_SH	0x6


# This is the bootloader. It is loaded by the BIOS into physical memory
# at address 0x7c00. The BIOS starts executing this in real mode 
# with %cs=0 and %eip=0x7c00.
# 
# What it does:
#	*disables interrupts and sets incrementing string operations
#	*sets up segment registers
#	*enables address line 20
#	*detect how much RAM is available (Simple with INT 0x15 and EAX = 0xE801)
#	*constructs and loads a trivial GDT
#	*enables protected mode
#	*sets up segment registers
#	*loads the first page from the harddisk
#	*checks that it contains an ELF executable
#	*reads the ELF header that is loaded into memory
#	*determines how many segments are in the ELF
#	*starts loading the ELF segments one by one
#		*reads the physical address from the program header,
#		 loads it into EDI and rounds it down to a sector boundary
#		*reads the offset(in the file) from the program header,
#		 loads it into EBX and converts it to sectors
#		 (which also rounds it dow to sector boundary)
#		*reads the size of the segment from the program header
#		 and converts it ot sectors, rounding up (so it doesn't load too little)
#		*puts EBX on the stack for the read rutine
#		*reads the required number of disk segments
#		*moves to the next ELF segment
#	*reads the entry point from the ELF header and calls it


.globl start
start:
	.code16						# Assemble for 16-bit
	cli							# Disable interrupts
	cld							# String operations increment
	
	# Set up segment registers DS, ES, SS.
	xorw	%ax,%ax				# Set AX register to zero
	movw	%ax,%ds				# Copy zero into DS
	movw	%ax,%es				# Copy zero into ES
	movw	%ax,%ss				# Copy zero into SS
	
	# Enable address line 20. By default this line is held low for
	# backward compatibility reasons. It can be enabled by writing into
	# the keyboard port.
enable20.1:
	inb		$0x64,%al			# Wait for not-busy
	test	$0x2,%al
	jnz		enable20.1
	
	movb	$0x1d,%al			# Write 0x1d to port 0x64
	outb	%al,$0x64
	
enable20.2:
	inb		$0x64,%al			# Wait for not-busy
	test	$0x2,%al
	jnz		enable20.2
	
	movb	$0xdf,%al			# Write 0xdf to port 0x60
	outb	%al,$0x60
	
	# Before calling the kernel find out how much RAM exists.
	call	memdet

	# Load a GDT that defines a trivial mapping between linear and
	# physical addresses.
	lgdt	gdtreg
	
	# Set protected mode flag in control register zero.
	movl	%cr0,%eax
	orl		$CR0_PE,%eax
	movl	%eax,%cr0

	# Set the code segment register by making a long jump. From now on
	# the processor is completely in protected mode.
	ljmp	$PROT_CSEG, $start32
	
start32:
	.code32						# Assemble for 32-bit now
	
	# Set up all the segment registers except CS which we already did
	# with the long jump. All the segment registers point to the same
	# segment selector in the GDT.
	movw	$PROT_DSEG,%ax
	movw	%ax,%ds
	movw	%ax,%es
	movw	%ax,%fs
	movw	%ax,%gs
	movw	%ax,%ss
	
	# Set up the stack. And clear EAX register.
	movl	$start,%esp
	xorl	%eax,%eax
	
	# Now start loading the kernel. The ELF header first.
	movl	$KERNEL_HDR,%edi	# Location to load kernel header
	pushl	$KERNEL_START		# Kernel starts at second sector
	movl	$PAGE_SECTS,%ecx	# Load one page - 8 sectors

load_head:
		call 	read
		addl	$0x1,(%esp)		# Increment read offset
	loop load_head
	addl	$0x4,%esp			# Remove parameter from stack
	
	# Check if what is loaded is a valid ELF executable
	movl	$ELF_MAGIC,%eax		# First four bytes should be ELF_MAGIC
	cmp		KERNEL_HDR,%eax
	jnz		spin				# If not just spin
	
	# Now load the segments of kernel. First check how many are there.
	movw	(KERNEL_HDR + E_PHNUM),%cx	# Number of segments to load
	movl	(KERNEL_HDR + E_PHOFF),%eax	# Address of first segment entry
load_seg:	
		# Physical address of segment
		movl	(KERNEL_HDR + P_PADDR)(%eax),%edi
		andl	$SECT_ROUND,%edi		# Round down to sector boundary
		# Offset of segment in bytes
		movl	(KERNEL_HDR + P_OFFSET)(%eax),%ebx
		shr		$SECT_SH,%ebx			# Bytes to sectors (this rounds down)
		addl	$KERNEL_START,%ebx		# Kernel starts at second sector	
		pushl	%ecx					# Save loop index
		# Memory size in bytes
		movl	(KERNEL_HDR + P_MEMSZ)(%eax),%ecx		
		shr		$SECT_SH,%ecx			# Bytes to sectors(rounded down)
		addl	$0x1,%ecx				# Round up
		pushl	%ebx					# Parameter for read routine
		
	load_sect:
			call	read
			addl	$0x1,(%esp)			# Increment read offset
		loop load_sect
		addl	$0x4,%esp				# Remove parameter from stack
		popl	%ecx					# Restore loop index
		
		addw	(KERNEL_HDR + E_PHENTSIZE),%ax		# Next segment
	loop load_seg
	
	# Put the amount of RAM above 1MB (in MB) into the EBX register.
	# The kernel will use this until it has a better picture.
	movl	memdetsize,%ebx
	
	# Call "entry" which finally starts the kernel. It should never
	# return.
	movl	(KERNEL_HDR + E_ENTRY),%eax 			
	call	*%eax						
	
spin:
	jmp		spin


# Read a sector from the harddisk
# Parameters (in order pushed on stack)
#	*offset in number of sectors
# Notes
#	*location of output has to be in EDI register
#	*EDI register is changed on return
read:
	pushl	%eax				# Save EAX register
	
	movw	$IDE_ERROR,%dx		# Set LBA28 addressing
	movb	$0x0,%al
	outb	%al,(%dx)
	
	movl	8(%esp),%eax		# Load parameter from stack - offset
	
	movw	$IDE_SNUM,%dx		# Lowest byte of LBA28 address
	outb	%al,(%dx)
	
	movw	$IDE_CYL_L,%dx		# Second byte of LBA28 address
	shr		$BYTE_SH,%eax		
	outb	%al,(%dx)
	
	movw	$IDE_CYL_H,%dx		# Third byte of LBA28 address
	shr		$BYTE_SH,%eax			
	outb	%al,(%dx)

	movw	$IDE_HEAD,%dx		# Highest four bits of LBA28 address
	shr		$BYTE_SH,%eax
	orb		%al,0xE0			# Neccesary flags
	outb	%al,(%dx)
	
	movw	$IDE_SCOUNT,%dx		# Sector count
	movb	$0x1,%al			# Transfer one sector at a time
	outb	%al,(%dx)
	
	call	waitdisk
	
	movw	$IDE_CMD,%dx		# Command register
	movb	$IDE_CMD_READ,%al	# Read sectors command
	outb	%al,(%dx)
	
	pushl	%ecx				# Save counting register
	movl	$SECT_LONGS,%ecx	# Number of longs in a sector
	movw	$IDE_DATA,%dx		# Data register
	rep		insl
	popl	%ecx				# Restore counting register

	popl	%eax				# Restore EAX register	
	ret


# Wait for disk to be ready for reading
waitdisk:
	pushl	%eax				# Save EAX register
	movw	$IDE_CMD,%dx		
	
busy:
	inb		(%dx),%al
	andb	$0xC0,%al
	cmp		$0x40,%al			# Disk has to be ready and not busy
	jne		busy

	popl	%eax				# Restore EAX register	
	ret

# Simple way of detecting the amount of RAM on the machine.
memdet:
	.code16	
	memdet_chk:	
		movl	$MEM_DET_CMD,%eax	# INT 0x15 command
		# Result will be either in ECX, EDX or EAX, EBX. Depends on BIOS.
		xorl	%ebx,%ebx
		xorl	%ecx,%ecx
		xorl	%edx,%edx
		int		$0x15			# BIOS interrupt
		jcxz	memdet_end		# If ECX=0 then we are done, results are in EAX
		
		movl	%ecx,%eax
		movl	%edx,%ebx
	
	# EAX = number of contiguous kB, 1M to 16M
	# EBX = contiguous 64kB pages above 16M
	# Combine the detected amount (converted to kB).
	memdet_end:
		shl		$MEM_DET_PG_SH,%ebx
		addl	%ebx,%eax
		movl	%eax,memdetsize
		ret
	.code32

memdetsize:
	.word	0x0
	.word	0x0

	# GDT register is 6 bytes long. The first word contains the length
	# of the GDT minus one. The rest contains the address of the GDT.
gdtreg:
	.word	0x17
	.long	gdt
	
	# GDT is made of entries that are 8 bytes long. The mapping is
	# trivial, linear address = physical address, for both the code
	# segment and the data segment.
.p2align 2						# Force 4 byte alignment
gdt:
	SEG_NULL
	SEG(STA_R|STA_X, 0x0, 0xffffffff)
	SEG(STA_W, 0x0, 0xffffffff)

	# Move to the end of the bootblock. The last two bytes of the
	# bootblock should be 0xAA55 to signinify that the drive is
	# bootable.
	.org	BLOCK_LEN
	.word	BOOT_SIG
